package cn.ant.config;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

/***
 * RabbitMQ配置发布回调方法
 * @author Anhui AntLaddie <a href="https://juejin.cn/user/4092805620185316">(掘金蚂蚁小哥)</a>
 * @version 1.0
 **/
@Slf4j
@Component
@RequiredArgsConstructor
public class RabbitMQMyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {

    // 注入rabbitTemplate对象
    private final RabbitTemplate rabbitTemplate;

    /***
     * 对象实例化完成（对象创建和属性注入）后调用此方法
     */
    @PostConstruct
    public void init() {
        // 设置发布确认信息回调类RabbitTemplate.ConfirmCallback confirmCallback;
        rabbitTemplate.setConfirmCallback(this);
        // 设置回退消息回调类ReturnsCallback.ReturnsCallback returnsCallback;
        rabbitTemplate.setReturnsCallback(this);
        // true：交换机无法将消息进行路由时，会将该消息返回给生产者；
        // false：如果发现消息无法进行路由，则直接丢弃。
        rabbitTemplate.setMandatory(true);  // 或使用配置 spring.rabbitmq.template.mandatory: true
    }

    @Override
    public void confirm(@NonNull CorrelationData correlationData, boolean ack, @NonNull String cause) {
        // 断言判空
        Assert.notNull(correlationData, "回调方法 correlationData 参数不可为空！");
        // 获取对应的ID消息
        String id = correlationData.getId();
        // 校验是否成功发送
        if (ack) {
            log.info("消息已经成功交给了交换机，对应消息ID为：{}", id);
        } else {
            // 若写了幂等性，在这里如果失败了不打算继续投递了，别忘了删除幂等性！！！
            // 生产者投递到交换机失败则需要有应对方式，这里我就不写具体的了：
            //      比如：记录日志（持久化）、消息重试机制、告警通知、手动补偿机制（可选）
            log.info("消息未能成功发送给交换机，对应消息ID为：{}，异常原因：{}", id, cause);
            // 如果使用Redis来记录失败投递则推荐做法：使用“本地消息表 + 状态机”模式
            // Redis存储信息结构包含：
            //      message_id：消息唯一标识
            //      status：状态，如待发送、发送成功、发送失败
            //      retry_count：当前重试次数
            //      next_retry_time：下一次重试时间
            // 每次发送失败都需要往上累计次数；成功则需要删除这个记录表。
            // 从生产者发送端就需要构建这个表信息保存到Redis中，存在则不存储。
        }
    }

    /***
     * 当消息无法被路由时执行当前回调（增加的路由失败回退机制）
     * 注：若配置了备份交换机则这里就不会执行了
     * @param returned 被退回的消息信息
     */
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        // 发送的消息
        Message message = returned.getMessage();
        // 发送到哪个交换机
        String exchange = returned.getExchange();
        // 交换机到队列的路由key
        String routingKey = returned.getRoutingKey();
        // 退回原因
        String replyText = returned.getReplyText();
        // 退回原因状态码
        int replyCode = returned.getReplyCode();
        //消息打印
        log.info("信息被回退，从交换机：{} 路由：{} 发送到队列失败，发送信息为：{}，退回状态码：{}和原因：{}",
                exchange, routingKey, message, replyCode, replyText);
        // 我们可以在这后面对发送失败的消息进行处理...；比如放到一个统一的队列，或者打印日志记录；
    }
}
